# *-coding:utf-8*#

# 先导入Python的标准模块
import os
import platform
import sys  # 提供sys.argv

# 其后导入第三方模块
from PyQt5.QtCore import (PYQT_VERSION_STR, QFile, QFileInfo, QSettings, QT_VERSION_STR, QTimer, QVariant, Qt)
from PyQt5.QtWidgets import (QAction, QActionGroup, QApplication, QDockWidget, QFileDialog, QFrame, QInputDialog,
                             QLabel, QListWidget, QMainWindow, QMessageBox, QSpinBox)
from PyQt5.QtGui import (QIcon, QImage, QImageReader, QImageWriter, QKeySequence, QPixmap, QPainter)
from PyQt5.QtPrintSupport import QPrinter, QPrintDialog

# 最后导入自定义的模块
import helpform  # 提供HelpForm类
import newimagedlg  # 提供NewImageDlg类
import resizedlg
import resources

# 版本字符串，通常都会通过调用__version__来实现，会用在关于菜单项中
__version__ = "1.0.0"


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        self.image = QImage()  # null 的QImage,用来保存用户加载或者创建的图片，QImage不是QObject的子类，所以不需要父对象
        self.dirty = False  # dirty:图片是否存在未保存修改的标志
        self.filename = None  # 说明没有图片，或者是尚未未保存的新创建图片
        self.mirroredvertically = False  # 图片垂直镜像
        self.mirroredhorizontally = False  # 图片水平镜像

        # 中心窗口部件，用一个QLabel，QLabel可以显示普通文本、HTML或者是PyQt所支持的任意格式的图片
        self.imageLabel = QLabel()
        self.imageLabel.setMinimumSize(200, 200)  # 标签没有任何可以显示的内容，所以为其设置一个最小的尺寸
        self.imageLabel.setAlignment(Qt.AlignCenter)  # 垂直和水平居中对齐
        self.imageLabel.setContextMenuPolicy(Qt.ActionsContextMenu)  # 为窗口部件设置相应的上下文菜单策略。
        # 最为简单最为常用的方法。#
        # 必须向该窗口部件添加一些动作。当用户调用上下文菜单时，菜单就会弹出来，显示添加过的那些动作。
        self.setCentralWidget(self.imageLabel)  # 对话框用布局，主窗口风格有的只是一个中心窗口部件，对主窗口中心区域的窗口部件进行布局，又可以重新定义该窗口的父对象。

        # 停靠窗口是类的实例
        logDockWidget = QDockWidget("Log", self)  # 窗口标题，父对象
        logDockWidget.setObjectName("LogDockWidget")  # 对象名
        logDockWidget.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea|Qt.BottomDockWidgetArea)  # 限制它的停靠区域
        # logDockWidget.setfeatures()  控制停靠窗口部件是否可以移动、浮动、关闭等
        self.listWidget = QListWidget()  # 创建停靠窗口包含的窗口部件，这里是列表窗口部件
        logDockWidget.setWidget(self.listWidget)  # 停靠窗口可以添加一个部件，当然可以是复合部件。
        self.addDockWidget(Qt.RightDockWidgetArea, logDockWidget)  # 把停靠窗口部件添加到主窗口中。

        self.printer = None  # 将用于创建一个QPrinter对象

        self.sizeLabel = QLabel()  # 创建一个标签显示文件大小
        self.sizeLabel.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
        status = self.statusBar()
        status.setSizeGripEnabled(False)  # 关闭状态栏的尺寸大小拖拽符
        status.addPermanentWidget(self.sizeLabel)  # 将标签加到状态栏
        status.showMessage("Ready", 5000)  # 第二个参数是毫秒，5000毫秒=5秒，显示5秒后该条消息清除掉

        #   def createAction(self, text, slot=None, shortcut=None, icon=None, tip=None, checkable=False, signal="triggered"):
        fileNewAction = self.createAction("&New...", self.fileNew, QKeySequence.New, "filenew",
                                          "Create an image file")  # “文件--新建”动作
        fileOpenAction = self.createAction("&Open...", self.fileOpen, QKeySequence.Open, "fileopen",
                                           "Open an existing image file")  # 文件-打开
        fileSaveAction = self.createAction("&Save", self.fileSave, QKeySequence.Save, "filesave",
                                           "Save the image")  # 文件-保存
        fileSaveAsAction = self.createAction("Save &As...", self.fileSaveAs, icon="filesaveas",
                                             tip="Save the image using a new name")  # 文件保存
        filePrintAction = self.createAction("&Print", self.filePrint, QKeySequence.Print, "fileprint",
                                            "Print the image")  # 文件打印
        fileQuitAction = self.createAction("&Quit", self.close, "Ctrl+Q", "filequit", "Close the application")  # 退出
        # QKeySequenceo类并没有用于应用程序结束的的标准化快捷键，close()槽继续自QMainWindow
        editInvertAction = self.createAction("&Invert", self.editInvert, "Ctrl+I", "editinvert",
                                             "Invert the image's colors", True, "toggled")
        # 编辑撤销
        editSwapRedAndBlueAction = self.createAction("Sw&ap Red and Blue", self.editSwapRedAndBlue, "Ctrl+A",
                                                     "editswap",
                                                     "Swap the image's red and blue color components", True, "toggled")
        # 红蓝交换
        editZoomAction = self.createAction("&Zoom...", self.editZoom, "Alt+Z", "editzoom", "Zoom the image")
        # 让用户能够对图片放大或者缩小操作
        editResizeAction = self.createAction("&Resize...",
                                             self.editResize, "Ctrl+R", "editresize",
                                             "Resize the image")
        #自定义大小
        mirrorGroup = QActionGroup(self)  # 动作群组是QObject的一个子类。既不是一个顶级窗口也不是一个可以布局的窗口部件，所以必须明确地为其给定父对象。
        # 群组其实是一个类，可以管理一系列的可选型动作，并可确保所有动作只要有一个开，其他都为‘关’
        editUnMirrorAction = self.createAction("&Unmirror", self.editUnMirror, "Ctrl+U", "editunmirror",
                                               "Unmirror the image", True, "toggled")
        mirrorGroup.addAction(editUnMirrorAction)  # 添加动作到动作群组中。
        editMirrorHorizontalAction = self.createAction("Mirror &Horizontally", self.editMirrorHorizontal, "Ctrl+H",
                                                       "editmirrorhoriz",
                                                       "Horizontally mirror the image", True, "toggled")
        mirrorGroup.addAction(editMirrorHorizontalAction)
        editMirrorVerticalAction = self.createAction("Mirror &Vertically", self.editMirrorVertical, "Ctrl+V",
                                                     "editmirrorvert",
                                                     "Vertically mirror the image", True, "toggled")
        mirrorGroup.addAction(editMirrorVerticalAction)
        editUnMirrorAction.setChecked(True)

        helpAboutAction = self.createAction("&About Image Changer", self.helpAbout)  # 关于动作
        helpHelpAction = self.createAction("&Help", self.helpHelp, QKeySequence.HelpContents)  # 帮助动作

        # 我们希望文件(File)菜单能够显示最近用过的那些文件。在用户调用它的时候再动态地创建菜单。
        # 这就是为什么会创建一个文件菜单的实例变量来保存文件菜单的各个动作。
        self.fileMenu = self.menuBar().addMenu("&File")
        self.fileMenuActions = (  # 这里没有用addActions，是因为文件菜单是要动态生成的，生成之后再回到这里。
            fileNewAction, fileOpenAction, fileSaveAction, fileSaveAsAction, None, filePrintAction, fileQuitAction)
        self.fileMenu.aboutToShow.connect(self.updateFileMenu)

        editMenu = self.menuBar().addMenu("&Edit")
        self.addActions(editMenu, (editInvertAction, editSwapRedAndBlueAction, editZoomAction,editResizeAction))  # 添加动作到菜单栏上
        mirrorMenu = editMenu.addMenu(QIcon(":/editmirror.png"), "&Mirror")
        self.addActions(mirrorMenu, (editUnMirrorAction, editMirrorHorizontalAction, editMirrorVerticalAction))
        helpMenu = self.menuBar().addMenu("&Help")
        self.addActions(helpMenu, (helpAboutAction, helpHelpAction))

        fileToolbar = self.addToolBar("File")
        fileToolbar.setObjectName("FileToolBar")
        self.addActions(fileToolbar, (fileNewAction, fileOpenAction, fileSaveAsAction))  # 添加动作到工具栏上
        editToolbar = self.addToolBar("Edit")
        editToolbar.setObjectName("EditToolBar")
        self.addActions(editToolbar, (
            editInvertAction, editSwapRedAndBlueAction, editUnMirrorAction, editMirrorVerticalAction,
            editMirrorHorizontalAction))
        # 新建一个微调框，调整缩放因子
        self.zoomSpinBox = QSpinBox()
        self.zoomSpinBox.setRange(1, 400)
        self.zoomSpinBox.setSuffix(" %")
        self.zoomSpinBox.setValue(100)
        self.zoomSpinBox.setToolTip("Zoom the image")
        self.zoomSpinBox.setStatusTip(self.zoomSpinBox.toolTip())
        self.zoomSpinBox.setFocusPolicy(Qt.NoFocus)
        self.zoomSpinBox.valueChanged.connect(self.showImage)

        editToolbar.addWidget(self.zoomSpinBox)  # 加到工具栏上。

        self.addActions(self.imageLabel, (
            editInvertAction, editSwapRedAndBlueAction, editUnMirrorAction, editMirrorVerticalAction,
            editMirrorHorizontalAction))

        self.resetableActions = (
            (editInvertAction, False), (editSwapRedAndBlueAction, False), (editUnMirrorAction, True))

        # 开始恢复系统设置
        # restoreState,saveState方法，保存和恢复停靠窗口大小，位置，工具栏的位置等
        settings = QSettings()  # 不加参数，会使用main中保存的名字
        self.recentFiles = settings.value("RecentFiles") or []
        setting_Geometry = settings.value("MainWindow/Geometry")
        if setting_Geometry is not None:
            self.restoreGeometry(setting_Geometry)
            # Restores the geometry and state of top-level widgets stored in the byte array geometry.
            # Use QMainWindow::restoreState() to restore the geometry and the state of toolbars and dock widgets.
        setting_State = settings.value("MainWindow/State")
        if setting_State is not None:
            self.restoreState(setting_State)

        self.setWindowTitle("Image Changer")
        self.updateFileMenu()
        # 文件菜单动态主动创建.因为前面‘aboutToShow（）’有一次触发了。但时间可能不足以创建菜单，所以在此加上个明确的调用。
        QTimer.singleShot(0, self.loadInitialFile)  # 如果需要在启动的时候处理一些事情，如需要加载许多较大的文件，通常总会在一个单独的加载方法中来
        # 处理这些事情。在主窗体构造函数的最后，会通过一个0时单触发定时器来调用这个加载函数。
        # 加载函数并不会将这个过程全部完成才显示，这样会让用户等待。
        # 所以实际上是将后面方法交给队列就返回了，这样会先看到主窗口，然后看到在主窗口中加载大文件，用户体验较好。

    # 创建和使用动作，创建动作的程序助手
    # createAction(self,name:text,slotName,shortcut,icon,tip,checkable,signal
    def createAction(self, text, slot=None, shortcut=None, icon=None, tip=None, checkable=False, signal="triggered"):
        action = QAction(text, self)  # self作为父对象。
        if icon is not None:
            action.setIcon(QIcon(":/{0}.png".format(icon)))
        if shortcut is not None:
            action.setShortcut(shortcut)
        if tip is not None:
            action.setToolTip(tip)
            action.setStatusTip(tip)
        if slot is not None:
            getattr(action, signal).connect(slot)  # getattr(x,'y')==x.y；等于 获得类的属性，这里如 FileNew.triggered
        #            self.connect(action, SIGNAL(signal), slot)
        if checkable:
            action.setCheckable(True)
        return action

    def addActions(self, target, actions):  # target是一个菜单或者工具栏，actions 是一个列表或者是动作或None的元组。
        for action in actions:
            if action is None:
                target.addSeparator()
            else:
                target.addAction(action)
    def editResize(self):
        if self.image.isNull():
            return
        form = resizedlg.ResizeDlg(self.image.width(),
                                   self.image.height(), self)
        if form.exec_():
            width, height = form.result()
            if (width == self.image.width() and
                height == self.image.height()):
                self.statusBar().showMessage("Resized to the same size",
                                             5000)
            else:
                self.image = self.image.scaled(width, height)
                self.showImage()
                self.dirty = True
                size = "{0} x {1}".format(self.image.width(),
                                          self.image.height())
                self.sizeLabel.setText(size)
                self.updateStatus("Resized to {0}".format(size))
    def closeEvent(self, event):  # 从文件菜单退出，或ctrl+Q退出，会调用基类的close方法，但是点击X则不会，所以必须覆盖基类方法的“closeEvent”方法
        """退出事件处理，保存当前文件，修改最近文件列表，大小和位置，状态"""
        if self.okToContinue():
            settings = QSettings()
            filename = self.filename if self.filename is not None else ""
            settings.setValue("LastFile", filename)  # 保存最后的文件，在下次启动时先加载。
            recentFiles = self.recentFiles if self.recentFiles else []
            settings.setValue("RecentFiles", recentFiles)
            settings.setValue("MainWindow/Geometry", self.saveGeometry())
            settings.setValue("MainWindow/State",
                              self.saveState())  # Saves the current state of this mainwindow's toolbars and dockwidgets
        else:
            event.ignore()

    def okToContinue(self):
        """如果文件有修改，用来确认是否保存，如果保存，转入保存方法，然后继续，否则即不作保存，继续下一步"""
        if self.dirty:
            reply = QMessageBox.question(self, "Image Changer - Unsaved Changes", "Save unsaved changes?",
                                         QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
            if reply == QMessageBox.Cancel:
                return False
            elif reply == QMessageBox.Yes:
                return self.fileSave()
        return True

    def loadInitialFile(self):
        settings = QSettings()
        fname = settings.value("LastFile")
        if fname and QFile.exists(fname):
            self.loadFile(fname)

    def updateStatus(self, message):
        self.statusBar().showMessage(message, 5000)
        self.listWidget.addItem(message)
        if self.filename is not None:
            self.setWindowTitle("Image Changer - {0}[*]".format(os.path.basename(self.filename)))
        elif not self.image.isNull():
            self.setWindowTitle("Image Changer - Unnamed[*]")
        else:
            self.setWindowTitle("Image Changer[*]")
        self.setWindowModified(self.dirty)

    def updateFileMenu(self):
        """动态增加生成文件菜单"""
        self.fileMenu.clear()  # 清除
        self.addActions(self.fileMenu, self.fileMenuActions[:-1])  # 将最后一项之外的加上，close
        current = (self.filename if self.filename is not None else None)
        recentFiles = []
        for fname in self.recentFiles:
            if fname != current and QFile.exists(fname):  # 如果这个文件不是当前文件，而且存在（未删除）
                recentFiles.append(fname)
        if recentFiles:
            self.fileMenu.addSeparator()
            for i, fname in enumerate(recentFiles):
                action = QAction(QIcon(":/icon.png"), "&{0} {1}".format(i + 1, QFileInfo(fname).fileName()), self)
                # action.setData(QVariant(fname))
                action.setData(fname)
                # print(type(fname))#str
                action.triggered.connect(self.loadFile)

                self.fileMenu.addAction(action)
        self.fileMenu.addSeparator()
        self.fileMenu.addAction(self.fileMenuActions[-1])

    def fileNew(self):
        if not self.okToContinue():
            return
        dialog = newimagedlg.NewImageDlg(self)
        if dialog.exec_():
            self.addRecentFile(self.filename)
            self.image = QImage()
            for action, check in self.resetableActions:
                action.setChecked(check)
            self.image = dialog.image()
            self.filename = None
            self.dirty = True
            self.showImage()
            self.sizeLabel.setText("{0} x {1}".format(self.image.width(), self.image.height()))
            self.updateStatus("Created new image")

    def fileOpen(self):
        if not self.okToContinue():
            return
        dir = (os.path.dirname(self.filename)
               if self.filename is not None else ".")
        formats = (
            ["*.{0}".format(format.data().decode('utf-8').lower()) for format in QImageReader.supportedImageFormats()])
        # QImageReader.supportedImageFormats返回的是QbyteArray类型，上面的data，在此似乎是提取了字节数据，解码，改小写才完成的。
        # fname = unicode(QFileDialog.getOpenFileName(self, "Image Changer - Choose Image", dir, "Image files ({0})".format(" ".join(formats))))
        fname, _ = QFileDialog.getOpenFileName(self, "Image Changer - Choose Image", dir,
                                               "Image files ({0})".format(" ".join(formats)))
        if fname:
            self.loadFile(fname)

    def loadFile(self, fname=None):
        if not fname:
            # if fname is None:
            action = self.sender()
            if isinstance(action, QAction):
                fname = action.data()
                if not self.okToContinue():
                    return
            else:
                return
        if fname:
            self.filename = None
            image = QImage(fname)
            if image.isNull():
                message = "Failed to read {0}".format(fname)
            else:
                self.addRecentFile(fname)
                self.image = QImage()
                for action, check in self.resetableActions:
                    action.setChecked(check)
                self.image = image
                self.filename = fname
                self.showImage()
                self.dirty = False
                self.sizeLabel.setText("{0} x {1}".format(image.width(), image.height()))
                message = "Loaded {0}".format(os.path.basename(fname))
            self.updateStatus(message)

    def addRecentFile(self, fname):
        # print(type(fname))这里仍是str
        if fname is None:
            return
        if not self.recentFiles.__contains__(fname):  # .contains(fname):
            self.recentFiles.append(fname)
            while len(self.recentFiles) > 9:
                self.recentFiles.pop(0)  # 修改了原来的takeLast()的错误

    def fileSave(self):
        if self.image.isNull():
            return True
        if self.filename is None:
            return self.fileSaveAs()
        else:
            if self.image.save(self.filename, None):
                self.updateStatus("Saved as {0}".format(self.filename))
                self.dirty = False
                return True
            else:
                self.updateStatus("Failed to save {0}".format(
                    self.filename))
                return False

    def fileSaveAs(self):
        if self.image.isNull():
            return True
        fname = self.filename if self.filename is not None else "."
        formats = (["*.{0}".format(format.data().decode('utf-8').lower())
                    for format in QImageWriter.supportedImageFormats()])
        fname, _ = QFileDialog.getSaveFileName(self, "Image Changer - Save Image", fname,
                                               "Image files ({0})".format(" ".join(formats)))
        if fname:
            if "." not in fname:
                fname += ".png"  # finished
            self.addRecentFile(fname)  # bug在此
            self.filename = fname
            return self.fileSave()
        return False

    def filePrint(self):
        if self.image.isNull():
            return
        if self.printer is None:
            self.printer = QPrinter(QPrinter.HighResolution)
            self.printer.setPageSize(QPrinter.Letter)
        form = QPrintDialog(self.printer, self)
        if form.exec_():
            painter = QPainter(self.printer)
            rect = painter.viewport()
            size = self.image.size()
            size.scale(rect.size(), Qt.KeepAspectRatio)
            painter.setViewport(rect.x(), rect.y(), size.width(),
                                size.height())
            painter.drawImage(0, 0, self.image)

    def editInvert(self, on):
        if self.image.isNull():
            return
        self.image.invertPixels()
        self.showImage()
        self.dirty = True
        self.updateStatus("Inverted" if on else "Uninverted")

    def editSwapRedAndBlue(self, on):
        if self.image.isNull():
            return
        self.image = self.image.rgbSwapped()
        self.showImage()
        self.dirty = True
        self.updateStatus(("Swapped Red and Blue" if on else "Unswapped Red and Blue"))

    def editUnMirror(self, on):
        if self.image.isNull():
            return
        if self.mirroredhorizontally:
            self.editMirrorHorizontal(False)
        if self.mirroredvertically:
            self.editMirrorVertical(False)

    def editMirrorHorizontal(self, on):
        if self.image.isNull():
            return
        self.image = self.image.mirrored(True, False)
        self.showImage()
        self.mirroredhorizontally = not self.mirroredhorizontally
        self.dirty = True
        self.updateStatus(("Mirrored Horizontally" if on else "Unmirrored Horizontally"))

    def editMirrorVertical(self, on):
        if self.image.isNull():
            return
        self.image = self.image.mirrored(False, True)
        self.showImage()
        self.mirroredvertically = not self.mirroredvertically
        self.dirty = True
        self.updateStatus(("Mirrored Vertically" if on else "Unmirrored Vertically"))

    def editZoom(self):  # 这是菜单上的比例对话框。
        if self.image.isNull():
            return
        percent, ok = QInputDialog.getInt(self, "Image Changer - Zoom", "Percent:", self.zoomSpinBox.value(), 1, 400)
        if ok:
            self.zoomSpinBox.setValue(percent)

    def showImage(self, percent=None):
        if self.image.isNull():
            return
        if percent is None:
            percent = self.zoomSpinBox.value()
        factor = percent / 100.0
        width = self.image.width() * factor
        height = self.image.height() * factor
        image = self.image.scaled(width, height, Qt.KeepAspectRatio)
        self.imageLabel.setPixmap(QPixmap.fromImage(image))

    def helpAbout(self):
        QMessageBox.about(self, "About Image Changer",
                          """<b>Image Changer</b> v {0}
                          <p>Copyright &copy; 2008 Qtrac Ltd. 
                          All rights reserved.
                          <p>This application can be used to perform
                          simple image manipulations.
                          <p>Python {1} - Qt {2} - PyQt {3} on {4}""".format(
                              __version__, platform.python_version(),
                              QT_VERSION_STR, PYQT_VERSION_STR,
                              platform.system()))

    def helpHelp(self):
        form = helpform.HelpForm("index.html", self)
        form.show()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setOrganizationName("Qtrac Ltd.")
    app.setOrganizationDomain("qtrac.eu")
    app.setApplicationName("Image Changer")
    app.setWindowIcon(QIcon(":/icon.png"))

    # 目的是为了加载和保存应用程序的设置。在开始之前，需要恢复应用程序从前一次运行的设置，或者恰好是应用程序第一次运行的话，就使用其默认设置。
    form = MainWindow()
    form.show()
    app.exec_()

